/**
 * @author Ahammer
 * @date   2009 04
 * @update 2010 10 added blob erasion
 * @update 2011 01 added 16 bit support (not for Skeleton, Fill Holes and Erase Blobs)
 */
/*
 * The Skeleton and the Fill Holes options are carried out by launching ImageJ
 * and by using the corresponding plugins. 
 * http://rsbweb.nih.gov/ij/
 */

/* erasion of blobs is carried out by using the BlobFinder class
 * by A.Greensted http://www.labbookpages.co.uk
 */

/*
 * This file is part of Iqm.
 * Copyright (c) 2010-2011 Helmut Ahammer
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package op;

import ij.IJ;
import ij.ImageJ;
import ij.ImagePlus;
import ij.WindowManager;
import ij.gui.Toolbar;
import ij.plugin.filter.ParticleAnalyzer;
import ij.process.ImageProcessor;
import ij.process.ImageStatistics;
import java.awt.Color;
import java.awt.Image;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.PixelInterleavedSampleModel;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.awt.image.renderable.ParameterBlock;
import java.util.ArrayList;
import java.util.Arrays;
import javax.media.jai.BorderExtender;
import javax.media.jai.JAI;
import javax.media.jai.KernelJAI;
import javax.media.jai.ParameterBlockJAI;
import javax.media.jai.PlanarImage;
import tools.BlobFinder;
import main.Board;
import main.IqmTools;

/**
 * This is the main image processing class
 * There exist two approaches:
 * A user defined JAI operator is just called 
 * or:
 * The actual processing is implemented in this class 
 */
public class IqmMorphOperator{ 


  public IqmMorphOperator() {
	  //WARNING: Don't declare fields here
	  //Fields declared here aren't thread safe!   
  }
  /**
   * 
   * @param ParametrBlockJAI pb
   */
  public Object run(ParameterBlockJAI pbJAI){
	  Object ob = null;
//    ob = JAI.create("IqmMorph".toLowerCase(), pb, null);  
	  
	  PlanarImage pi     = (PlanarImage) pbJAI.getRenderedSource(0);
	  int morph          = pbJAI.getIntParameter("Morph"); 
	  int kernelSize     = pbJAI.getIntParameter("KernelSize");
 	  int kernelShape    = pbJAI.getIntParameter("KernelShape");
 	  int times          = pbJAI.getIntParameter("Times");
	  double minBlobSize = pbJAI.getDoubleParameter("MinBlobSize");
	  double maxBlobSize = pbJAI.getDoubleParameter("MaxBlobSize");
	  	  
	  String type = IqmTools.getImgTyp(pi);
	  double typeGreyMax = IqmTools.getImgTypeGreyMax(pi);  	
	  String imgName = (String) pi.getProperty("image_name");
	  String fileName = (String) pi.getProperty("file_name");
	  
  	  KernelJAI kernelJAI = null;
	  int kernelWidth  = kernelSize;
	  int kernelHeight = kernelSize;
	  if (kernelShape == 0){
		  //kernelJAI = KernelFactory.createRectangle(kernelWidth, kernelHeight);
		  int size = kernelSize*kernelSize;
		  float[] kernel = new float[size];
		  Arrays.fill(kernel, 0.0f);  //Damit Hintergrund nicht auf 1 gesetzt wird
		  kernelJAI = new KernelJAI(kernelSize, kernelSize, kernel); 
	  }
	  if (kernelShape == 1){
		 //kernel = KernelFactory.createCircle((kernelSize-1)/2); //Size = radius*2 +1
		 int size = kernelSize*kernelSize;
		 float[] kernel = new float[size];
		 //Arrays.fill(kernel, 1.0f);
		 for (int i = 0; i < kernelSize; i++) {
				  for ( int j = 0; j < kernelSize; j++) {
					  if ((i == (kernelSize/2)) && (j == (kernelSize/2))) {
						  kernel[i*kernelSize + j] = 0.0f;
					  }else{
						  kernel[i*kernelSize + j] = 0.0f;					  
					  }
				  }
		  }
	      kernelJAI = new KernelJAI(kernelSize, kernelSize, kernel); 
	  }
	  
	  //Optional print kernel
	  //System.out.println(KernelUtil.kernelToString(kernelJAI, true));
	  
	  ParameterBlock pb = new ParameterBlock();
	  RenderingHints rh = new RenderingHints(JAI.KEY_BORDER_EXTENDER, BorderExtender.createInstance(BorderExtender.BORDER_COPY));
	  if (morph == 0){ //dilate
			pb.add(kernelJAI);
			for (int t = 0; t < times; t++){
				pb.addSource(pi);	
				pi = JAI.create("dilate", pb, rh);
				pb.removeSources();
			}
			ob = pi;
	  }
	  if (morph == 1){ //erode
			pb.add(kernelJAI);
			for (int t = 0; t < times; t++){
				pb.addSource(pi);	
				pi = JAI.create("erode", pb, rh);
				pb.removeSources();
			}
			ob = pi;
	  }
	  if (morph == 2){ //close
			pb.add(kernelJAI);
			for (int t = 0; t < times; t++){
				pb.addSource(pi);	
				pi = JAI.create("dilate", pb, rh);
				pb.removeSources();
				pb.addSource(pi);	
				pi = JAI.create("erode", pb, rh);
				pb.removeSources();
			}
			ob = pi;
	  }
	  if (morph == 3){ //open
			pb.add(kernelJAI);
			for (int t = 0; t < times; t++){
				pb.addSource(pi);	
				pi = JAI.create("erode", pb, rh);
				pb.removeSources();
				pb.addSource(pi);	
				pi = JAI.create("dilate", pb, rh);
				pb.removeSources();
			}
			ob = pi;
	  }
      
  	  if (morph == 4){ //this part uses ImageJ's binary sekeletonize
  		//PlanarImage pi = (PlanarImage) pb.getRenderedSource(0);
  		Image image = pi.getAsBufferedImage();
 		int dataType = pi.getSampleModel().getDataType();
		//new ImagePlus("", pi.getAsBufferedImage()).show();
		//ImagePlus imp = IJ.getImage();
		ImagePlus imp = new ImagePlus("", image);
	    if (imp!=null && (imp.getType()==ImagePlus.GRAY8 || imp.getType()==ImagePlus.COLOR_256)) {
	    	ImageStatistics stats = imp.getStatistics();
	        if (stats.histogram[0]+stats.histogram[255]!=stats.pixelCount) {
	        	Board.appendTexln("IqmMorphOperator: 8-bit binary (black and white only) image required");
	            return pi;
	        }
	    }
    
		//IJ.run(imp,"Invert","");
		IJ.run(imp,"Skeletonize","");
		//ImageJ instance = new ImageJ();
		//IJ.run(imp,"Invert","");
		imp.updateImage();
		//imp.updateAndDraw();
		//Image image = imp.getImage();
		Image image2 = imp.getBufferedImage();
		pi = JAI.create("AWTImage", image2);	
		ParameterBlock pb2 = new ParameterBlock();
	    pb2.addSource(pi);	
		pb2.add(dataType);  	
		ob = JAI.create("format",  pb2); //imp ist zb. bei RGB vom Typ int anstatt byte 
		((PlanarImage)ob).setProperty("image_name", imgName);
		imp.flush();
		//wr.setDataElements(minX, minY, pi.getData()); //
		//wr = (WritableRaster) pi.getData();   gehr hier nicht, da AWT image Tile Info verliert!!!!!!
	  }
	  if (morph == 5){  //this part uses ImageJ's hole filling
	  	//PlanarImage pi = (PlanarImage) pb.getRenderedSource(0);
		int dataType = pi.getSampleModel().getDataType();
		//new ImagePlus("", pi.getAsBufferedImage()).show();
		//ImagePlus imp = IJ.getImage();
		ImagePlus imp = new ImagePlus("", pi.getAsBufferedImage());
	    if (imp!=null && (imp.getType()==ImagePlus.GRAY8 || imp.getType()==ImagePlus.COLOR_256)) {
	    	ImageStatistics stats = imp.getStatistics();
	        if (stats.histogram[0]+stats.histogram[255]!=stats.pixelCount) {
	        	Board.appendTexln("IqmMorphOperator: 8-bit binary (black and white only) image required");
	            return pi;
	        }
	    }
		//IJ.run(imp,"Invert","");

		IJ.run(imp,"Fill Holes","");
		//IJ.run(imp,"Invert","");
		imp.updateImage();
		//imp.updateAndDraw();
		//Image image = imp.getImage();
		Image image = imp.getBufferedImage();
		pi = JAI.create("AWTImage", image);	
		ParameterBlock pb2 = new ParameterBlock();
	    pb2.addSource(pi);	
		pb2.add(dataType);  	
		ob = JAI.create("format",  pb2);              //imp ist zb. bei RGB vom Typ int anstatt byte
		((PlanarImage)ob).setProperty("image_name", imgName);
		((PlanarImage)ob).setProperty("file_name", fileName);	

		imp.flush();
		//wr.setDataElements(minX, minY, pi.getData()); //
		//wr = (WritableRaster) pi.getData();   gehr hier nicht, da AWT image Tile Info verliert!!!!!!
	  }

	  if (morph == 6){ //small blob elimination	

		  if (type == "RGB"){ //RGB
			  double[][] m = {{ 1./3, 1./3, 1./3, 0 }};
			  pb = new ParameterBlock();
			  pb.addSource(pi);
			  pb.add(m);
			  pi = JAI.create("bandcombine", pb, null);		  	
		  }
		  if (pi.getNumBands() != 1){
			  Board.appendTexln("IqmMorphOperator: error");
			  return pi;			  	
		  }
		  if (type == "16 bit"){ //16 bit
			  Board.appendTexln("IqmMorphOperator: 8-bit binary (black and white only) image required");
			  return pi;
		  }
		 
    	
		  int width  = pi.getWidth();
		  int height = pi.getHeight();
		
		  // Get raw image data
		  Raster raster = pi.getData();		  
		  DataBuffer buffer = raster.getDataBuffer();
		  DataBufferByte byteBuffer = (DataBufferByte) buffer;
		  byte[] srcData = byteBuffer.getData(0);
		  byte[] dstData = new byte[srcData.length*3];				
		  // Create Blob Finder
		  BlobFinder finder = new BlobFinder(width, height);
		  ArrayList<BlobFinder.Blob> blobList = new ArrayList<BlobFinder.Blob>();
		  //     detectBlobs(byte[] srcData, byte[] dstData, int minBlobMass, int maxBlobMass, byte matchVal, List<Blob> blobList)
		  int minBlobMass = new Double(Math.round((double)width*(double)height/100.0*minBlobSize)).intValue();
		  int maxBlobMass = new Double(Math.round((double)width*(double)height/100.0*maxBlobSize)).intValue();
		  //finder.detectBlobs(srcData, dstData, 0, -1, (byte)255, blobList);
		  System.out.println("IqmMorphOperator minBlobMass: "+minBlobMass);
		  System.out.println("IqmMorphOperator maxBlobMass: "+maxBlobMass);
		  finder.detectBlobs(srcData, dstData, minBlobMass, maxBlobMass, (byte)255, blobList);
		  // List Blobs
		  System.out.printf("Found %d blobs:\n", blobList.size());
		  for (BlobFinder.Blob blob : blobList) System.out.println(blob);
		  //Create outputImage (colored blobs)	  
		  DataBufferByte dataBuffer = new DataBufferByte(dstData, dstData.length);
		  int[] colOrder = new int[] {2,1,0};
		  SampleModel sampleModel = new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, width, height, 3, 3*width, colOrder);
		  ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
		  ComponentColorModel colourModel = new ComponentColorModel(cs, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
		  WritableRaster wr = Raster.createWritableRaster(sampleModel, dataBuffer, new Point(0,0));
		  BufferedImage bi = new BufferedImage(colourModel, wr, false, null);	  	  
		  ob = (PlanarImage)PlanarImage.wrapRenderedImage(bi);

		  //create a single banded grey image
		  double[][] m = {{ 1./3, 1./3, 1./3, 0 }};
		  pb = new ParameterBlock();
		  pb.addSource(ob);
		  pb.add(m);
	      ob = JAI.create("bandcombine", pb, null);
	      
	      //binarize
		  double low[]  = {1};
		  double high[] = {255};
		  double map[]  = {255};
		  pb = new ParameterBlock();
		  pb.addSource(ob).add(low).add(high).add(map);	
		  ob = JAI.create("threshold", pb, null);	
				  
		  ((PlanarImage)ob).setProperty("image_name", imgName);
		  ((PlanarImage)ob).setProperty("file_name", fileName);
			    
	  } 
	  return ob;
  }
}
